/*
 ------------------------------------------------------------------------------
 Copyright (C) 2006-2007 Team Blur.

 This file is part of the Quantum Engine source code.

 The Quantum Engine source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Quantum Engine source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Quantum Engine source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


#include "r_local.h"


#define PROGRAMS_HASH_SIZE				MAX_PROGRAMS / 4

typedef struct {
	const char *		name;
	uint				format;
	int					size;
	uniformType_t		type;
} uniformTable_t;

static uniformTable_t	r_uniformTable[] = {
	{"viewOrigin",			GL_FLOAT_VEC3,	1,	UT_VIEW_ORIGIN},
	{"viewAxis",			GL_FLOAT_MAT3,	1,	UT_VIEW_AXIS},
	{"entityOrigin",		GL_FLOAT_VEC3,	1,	UT_ENTITY_ORIGIN},
	{"entityAxis",			GL_FLOAT_MAT3,	1,	UT_ENTITY_AXIS},
	{"skyBoxMatrix",		GL_FLOAT_MAT4,	1,	UT_SKYBOX_MATRIX},
	{"portalSkyMatrix",		GL_FLOAT_MAT4,	1,	UT_PORTALSKY_MATRIX},
	{"screenMatrix",		GL_FLOAT_MAT4,	1,	UT_SCREEN_MATRIX},
	{"xyScale",				GL_FLOAT_VEC2,	1,	UT_XY_SCALE},
	{"potAdjust",			GL_FLOAT_VEC2,	1,	UT_POT_ADJUST},
	{"lightScale",			GL_FLOAT,		1,	UT_LIGHT_SCALE},
	{NULL,					0,				0,	UT_CUSTOM}
};

static program_t *		r_programsHashTable[PROGRAMS_HASH_SIZE];
static program_t *		r_programs[MAX_PROGRAMS];
static int				r_numPrograms;


/*
 ==============================================================================

 PROGRAM LOADING

 ==============================================================================
*/


/*
 ==================
 R_ParseProgramUniforms
 ==================
*/
static void R_ParseProgramUniforms (program_t *program){

	uniform_t		*activeUniform, activeUniforms[MAX_PROGRAM_UNIFORMS];
	int				numActiveUniforms;
	uniformTable_t	*uniform;
	uniformType_t	type;
	char			name[64];
	uint			format;
	int				size;
	int				location;
	int				i;

	// Bind the program
	qglUseProgram(program->programId);

	// Get active uniforms
	qglGetProgramiv(program->programId, GL_ACTIVE_UNIFORMS, &numActiveUniforms);
	if (!numActiveUniforms){
		qglUseProgram(0);
		return;
	}

	for (i = 0; i < numActiveUniforms; i++){
		qglGetActiveUniform(program->programId, i, sizeof(name), NULL, &size, &format, name);

		location = qglGetUniformLocation(program->programId, name);
		if (location == -1)
			continue;		// Built-in uniform

		// Check for internal uniform name
		for (uniform = r_uniformTable; uniform->name; uniform++){
			if (!Str_Compare(uniform->name, name))
				break;
		}

		if (uniform->name){
			if (uniform->format != format || uniform->size != size){
				Com_DPrintf(S_COLOR_YELLOW "Uniform '%s' in program '%s' has wrong format or size\n", name, program->name);
				continue;
			}

			type = uniform->type;
		}
		else {
			// We only support a limited set of custom uniform formats and size
			switch (format){
			case GL_BOOL:
			case GL_BOOL_VEC2:
			case GL_BOOL_VEC3:
			case GL_BOOL_VEC4:
			case GL_INT:
			case GL_INT_VEC2:
			case GL_INT_VEC3:
			case GL_INT_VEC4:
			case GL_FLOAT:
			case GL_FLOAT_VEC2:
			case GL_FLOAT_VEC3:
			case GL_FLOAT_VEC4:
			case GL_FLOAT_MAT2:
			case GL_FLOAT_MAT3:
			case GL_FLOAT_MAT4:
			case GL_SAMPLER_2D:
			case GL_SAMPLER_CUBE:
				break;
			default:
				Com_DPrintf(S_COLOR_YELLOW "Uniform '%s' in program '%s' has wrong format or size\n", name, program->name);
				continue;
			}

			if (size != 1){
				Com_DPrintf(S_COLOR_YELLOW "Uniform '%s' in program '%s' has wrong format or size\n", name, program->name);
				continue;
			}

			type = UT_CUSTOM;
		}

		// Add a new uniform
		if (program->numUniforms == MAX_PROGRAM_UNIFORMS)
			Com_Error(false, "R_ParseProgramUniforms: MAX_PROGRAM_UNIFORMS hit");

		activeUniform = &activeUniforms[program->numUniforms++];

		activeUniform->type = type;
		Str_Copy(activeUniform->name, name, sizeof(activeUniform->name));
		activeUniform->format = format;
		activeUniform->size = size;
		activeUniform->location = location;
	}

	// Allocate and copy the uniforms
	program->uniforms = (uniform_t *)Mem_Alloc(program->numUniforms * sizeof(uniform_t), TAG_RENDERER);
	memcpy(program->uniforms, activeUniforms, program->numUniforms * sizeof(uniform_t));

	// Unbind the program
	qglUseProgram(0);
}

/*
 ==================
 R_LinkProgram
 ==================
*/
static void R_LinkProgram (program_t *program){

	char	infoLog[MAX_PRINT_MESSAGE];
	int		infoLogLen;
	int		linkStatus;

	// Create the program
	program->programId = qglCreateProgram();

	// Attach the shaders
	if (program->vertexShader){
		qglAttachShader(program->programId, program->vertexShader->shaderId);

		program->vertexShader->references++;
	}

	if (program->fragmentShader){
		qglAttachShader(program->programId, program->fragmentShader->shaderId);

		program->fragmentShader->references++;
	}

	// Bind generic attributes
	qglBindAttribLocation(program->programId, 10, "va_Tangent1");
	qglBindAttribLocation(program->programId, 11, "va_Tangent2");

	// Link the program
	qglLinkProgram(program->programId);

	// Check for errors and print any to the console
	qglGetProgramiv(program->programId, GL_LINK_STATUS, &linkStatus);
	if (!linkStatus){
		Com_Printf(S_COLOR_RED "Failed to link program '%s'\n", program->name);

		qglGetProgramiv(program->programId, GL_INFO_LOG_LENGTH, &infoLogLen);
		if (infoLogLen > 0){
			qglGetProgramInfoLog(program->programId, sizeof(infoLog), NULL, infoLog);

			Com_DPrintf("------------------------------\n");
			Com_DPrintf("%s\n", infoLog);
			Com_DPrintf("------------------------------\n");
		}

		program->linked = false;

		return;
	}

	program->linked = true;

	// Parse the active uniforms
	R_ParseProgramUniforms(program);
}

/*
 ==================
 R_LoadProgram
 ==================
*/
static program_t *R_LoadProgram (const char *name, shader_t *vertexShader, shader_t *fragmentShader){

	program_t	*program;
	uint		hashKey;

	if (r_numPrograms == MAX_PROGRAMS)
		Com_Error(false, "R_LoadProgram: MAX_PROGRAMS hit");

	r_programs[r_numPrograms++] = program = (program_t *)Mem_Alloc(sizeof(program_t), TAG_RENDERER);

	// Fill it in
	Str_Copy(program->name, name, sizeof(program->name));
	program->vertexShader = vertexShader;
	program->fragmentShader = fragmentShader;
	program->numUniforms = 0;
	program->uniforms = NULL;

	R_LinkProgram(program);

	// Add to hash table
	hashKey = Str_HashKey(program->name, PROGRAMS_HASH_SIZE, false);

	program->nextHash = r_programsHashTable[hashKey];
	r_programsHashTable[hashKey] = program;

	return program;
}

/*
 ==================
 R_FindProgram
 ==================
*/
program_t *R_FindProgram (shader_t *vertexShader, shader_t *fragmentShader){

	program_t	*program;
	char		name[MAX_OSPATH];
	uint		hashKey;

	if (!vertexShader && !fragmentShader)
		Com_Error(false, "R_FindProgram: NULL vertexShader / fragmentShader combination");

	if (vertexShader && fragmentShader)
		Str_SPrintf(name, sizeof(name), "%s & %s", vertexShader->name, fragmentShader->name);
	else {
		if (vertexShader)
			Str_Copy(name, vertexShader->name, sizeof(name));

		if (fragmentShader)
			Str_Copy(name, fragmentShader->name, sizeof(name));
	}

	// See if already loaded
	hashKey = Str_HashKey(name, PROGRAMS_HASH_SIZE, false);

	for (program = r_programsHashTable[hashKey]; program; program = program->nextHash){
		if (program->vertexShader != vertexShader || program->fragmentShader != fragmentShader)
			continue;

		if (!Str_ICompare(program->name, name)){
			if (!program->linked)
				return NULL;

			return program;
		}
	}

	// Load the program
	program = R_LoadProgram(name, vertexShader, fragmentShader);

	if (!program->linked)
		return NULL;

	return program;
}


// ============================================================================


/*
 ==================
 R_GetProgramUniform
 ==================
*/
uniform_t *R_GetProgramUniform (program_t *program, const char *name){

	uniform_t	*uniform;
	int			i;

	for (i = 0, uniform = program->uniforms; i < program->numUniforms; i++, uniform++){
		if (!Str_Compare(uniform->name, name))
			return uniform;
	}

	return NULL;
}

/*
 ==================
 R_GetProgramUniformLocation
 ==================
*/
int R_GetProgramUniformLocation (program_t *program, const char *name, uint format, int size){

	uniform_t	*uniform;
	int			i;

	for (i = 0, uniform = program->uniforms; i < program->numUniforms; i++, uniform++){
		if (!Str_Compare(uniform->name, name) && uniform->format == format && uniform->size == size)
			return uniform->location;
	}

	return -1;
}

/*
 ==================
 R_SetProgramSampler
 ==================
*/
void R_SetProgramSampler (program_t *program, uniform_t *uniform, uint unit){

	qglUseProgram(program->programId);

	qglUniform1i(uniform->location, unit);

	qglUseProgram(0);
}

/*
 ==================
 R_SetProgramSamplerByName
 ==================
*/
void R_SetProgramSamplerByName (program_t *program, const char *name, uint unit){

	uniform_t	*uniform;
	int			i;

	for (i = 0, uniform = program->uniforms; i < program->numUniforms; i++, uniform++){
		if (!Str_Compare(uniform->name, name))
			break;
	}

	if (i == program->numUniforms)
		return;

	qglUseProgram(program->programId);

	qglUniform1i(uniform->location, unit);

	qglUseProgram(0);
}

/*
 ==================
 R_ListPrograms_f
 ==================
*/
static void R_ListPrograms_f (void){

	program_t	*program;
	int			i;

	Com_Printf("\n");
	Com_Printf("      parms linked -shaders--------\n");

	for (i = 0; i < r_numPrograms; i++){
		program = r_programs[i];

		Com_Printf("%4i: ", i);

		Com_Printf("%5i ", program->numUniforms);

		if (program->linked)
			Com_Printf("  yes  ");
		else
			Com_Printf("  no   ");

		Com_Printf("%s\n", program->name);
	}

	Com_Printf("-----------------------------------\n");
	Com_Printf("%i total programs\n", r_numPrograms);
	Com_Printf("\n");
}

/*
 ==================
 R_InitPrograms
 ==================
*/
void R_InitPrograms (void){

	// Register our commands
	Cmd_AddCommand("listPrograms", R_ListPrograms_f, "Lists loaded programs");
}

/*
 ==================
 R_ShutdownPrograms
 ==================
*/
void R_ShutdownPrograms (void){

	program_t	*program;
	int			i;

	// Unregister our commands
	Cmd_RemoveCommand("listPrograms");

	// Delete all programs
	qglUseProgram(0);

	for (i = 0; i < r_numPrograms; i++){
		program = r_programs[i];

		if (program->vertexShader){
			qglDetachShader(program->programId, program->vertexShader->shaderId);

			program->vertexShader->references--;
		}

		if (program->fragmentShader){
			qglDetachShader(program->programId, program->fragmentShader->shaderId);

			program->fragmentShader->references--;
		}

		qglDeleteProgram(program->programId);
	}

	// Clear program list
	memset(r_programsHashTable, 0, sizeof(r_programsHashTable));
	memset(r_programs, 0, sizeof(r_programs));

	r_numPrograms = 0;
}
